Skip to content

Conversation

@khellste
Copy link

@khellste khellste commented Oct 6, 2025

This PR implements a new @meta annotation similar to the one described in #102516 (comment).

The @meta(name: StringName, value: Variant = true) annotation allows script authors to tag classes and class members in their scripts with StringName-keyed arbitrary metadata. This metadata can then be inspected at runtime by calling one of the following new Script methods:

  • Array[StringName] Script.get_script_meta_list() const - gets a list of all @meta names appearing in the script.
  • Array[Dictionary] Script.get_script_meta(name: StringName) const - gets a list of dictionaries describing each appearance of @meta having the given name in the script.

Usage example

annotated_script.gd

@meta("meta1", [0, 1, 2, 3])
class_name MyOuterClass
extends Node

@meta("meta1")
var outer_prop: String

@meta("meta2", { key = "value" })
class MyInnerClass:
    extends RefCounted

    class MyInnerInnerClass:
        extends RefCounted

        @meta("meta2", false)
        var inner_inner_prop: String

introspector_script.gd

func _init() -> void:
    var annotated_script: Script = load("res://annotated_script.gd")

    # Prints: ["meta1", "meta2"]
    print(annotated_script.get_script_meta_list())

    # Prints: [{ &"target_container": &"", &"target_name": &"MyOuterClass", &"target_type": 0, &"value": [0, 1, 2, 3] }, { &"target_container": &"MyOuterClass", &"target_name": &"outer_prop", &"target_type": 1, &"value": true }]
    print(annotated_script.get_script_meta("meta1"))

    # Prints: [{ &"target_container": &"MyOuterClass", &"target_name": &"MyInnerClass", &"target_type": 0, &"value": { &"key": "value" } }, { &"target_container": &"MyOuterClass.MyInnerClass.MyInnerInnerClass", &"target_name": &"inner_inner_prop", &"target_type": 1, &"value": false }]
    print(annotated_script.get_script_meta("meta2"))

Specifications

  • The @meta annotation can target classes, constants, signals, properties, and methods. This includes arbitrarily deep inner classes and their members.
  • @meta can annotate a given target multiple times, even with the same name.
  • The second argument to @meta must be a constant expression. If omitted, it defaults to true.
  • Script.get_script_meta(name) returns a list of dictionaries, where each dictionary describes an instance of @meta(name, ...) in the script. The format of the dictionary is described in the docs added in bc46b32. It includes the target name, target type, container class name (if any), and metadata value.

Notes

  • @meta is a new builtin annotation and doesn't add support for custom user-defined annotations. However, I believe it should unblock most of the use cases in Allow custom GDScript annotations which can be read at runtime godot-proposals#1316.
  • @meta is for constant key-value metadata. The goal was not to implement something like Python decorators in Godot.
  • This PR only implements @meta for GDScript, but support for (e.g.) a companion C# [Meta(...)] attribute hooked up to the same getter interface on Script should be feasible.

@khellste khellste requested review from a team as code owners October 6, 2025 05:37
@arkology
Copy link
Contributor

arkology commented Oct 6, 2025

Having the uid as a separate file, but at the same time the metadata as an annotation inside the script looks really confusing.

@Mickeon
Copy link
Member

Mickeon commented Oct 6, 2025

In the attempt to solve godotengine/godot-proposals#1316 , I don't think it ever crossed most contributor's minds to utilize a single @meta tag as a custom tag. It does sound quite clever, but it's far too limiting in many occasions where implementing supplementary code is necessary.

@arkology
Copy link
Contributor

arkology commented Oct 6, 2025

Btw in your commits you ping user with @meta name, be careful.

@khellste
Copy link
Author

khellste commented Oct 6, 2025

Having the uid as a separate file, but at the same time the metadata as an annotation inside the script looks really confusing.

Unlike UIDs, the keys/values here are user-defined at the class and class-member level, so I think the text of the script is an appropriate place for them.

That said, I understand that calling this "metadata" could be confusing given that other kinds of script metadata (like UIDs) already exist and live elsewhere. The name was inspired by Object.get_meta but I have no objection to renaming this new annotation to @tag or similar if people prefer.

@khellste
Copy link
Author

khellste commented Oct 6, 2025

In the attempt to solve godotengine/godot-proposals#1316 , I don't think it ever crossed most contributor's minds to utilize a single @meta tag as a custom tag. It does sound quite clever, but it's far too limiting in many occasions where implementing supplementary code is necessary.

What application of custom annotations from the original proposal did you have in mind? I think this PR unblocks the original @Serialize case from that proposal. My intuition is that a good chunk of the more complex use cases ("I want to run some code when a certain annotation is present") can be accomplished by running the desired code in whatever script does the @meta introspecting.

@khellste
Copy link
Author

+@TokageItLab , since you took some interest in PR #102516.

@khellste
Copy link
Author

khellste commented Oct 28, 2025

In the attempt to solve godotengine/godot-proposals#1316 , I don't think it ever crossed most contributor's minds to utilize a single @meta tag as a custom tag. It does sound quite clever, but it's far too limiting in many occasions where implementing supplementary code is necessary.

What application of custom annotations from the original proposal did you have in mind? I think this PR unblocks the original @Serialize case from that proposal. My intuition is that a good chunk of the more complex use cases ("I want to run some code when a certain annotation is present") can be accomplished by running the desired code in whatever script does the @meta introspecting.

@Mickeon - to be more concrete, here's how @Serialize from the original proposal might be implemented using the @meta tag:

my_data_class.gd

class_name MyDataClass
extends RefCounted

@meta("serialize", { name = "json_foo" })
var foo: int

@meta("serialize")
var bar: String

var x: bool

main.gd

static func to_dict(data_object: RefCounted) -> Dictionary[StringName, Variant]:
    var result: Dictionary[StringName, Variant]
    var script = data_object.get_script() as Script
    if script == null: return result

    # Introspect @meta("serialize") annotations.
    for meta in script.get_script_meta("serialize"):
        if meta.target_type != Script.MetaTargetType.META_TARGET_VARIABLE:
            push_warning("ignoring @meta(\"serialize\") on non-property")
            continue
        var key: StringName =\
            meta.target_name if typeof(meta.value) == TYPE_BOOL and meta.value \
            else meta.value[&"name"]
        result[key] = data_object.get(meta.target_name)

    return result


func _init() -> void:
    var data_object := MyDataClass.new()
    data_object.foo = 0
    data_object.bar = "test"

    var dict := to_dict(data_object)
    print(dict) # { &"json_foo": 0, &"bar": "test" }

It's even powerful enough to implement something resembling the builtin @rpc annotation:

annotated_node.ts

extends Node

@meta("rpc", {
    rpc_mode = MultiplayerAPI.RPCMode.RPC_MODE_AUTHORITY,
    transfer_mode = MultiplayerPeer.TransferMode.TRANSFER_MODE_UNRELIABLE,
    call_local = false,
})
func foo() -> void: pass

main.ts (root node)

extends Node

static func apply_rpc_settings(node: Node) -> void:
    var script = node.get_script() as Script
    if script == null: return
    for meta in script.get_script_meta("rpc"):
        if meta.target_type != Script.MetaTargetType.META_TARGET_FUNCTION:
            push_warning("ignoring @meta(\"rpc\", ...) on non-method")
            continue
        node.rpc_config(meta.target_name, meta.value)

func _init() -> void:
    child_entered_tree.connect(apply_rpc_settings)

@TokageItLab
Copy link
Member

TokageItLab commented Oct 28, 2025

I agree the implementation as pure metadata, but I assume that this PR way seems quite indirect. So I feel that this way doesn't differ much from having the script have a reserved word dictionary.

As I mentioned in a previous PR #102516, my opinion is that this kind of implementation should be a little more invasive to the core and made readily available in C++/GDExtension and similar contexts.

In other words, I think implementing a function like ClassDB::bind_meta() in ClassDB would allow the class itself to hold the meta, rather than relying on scripts. Then @meta will be implemented as just syntactic sugar. And I think, the approach for implementing custom analyzers will likely resemble the registration process for EditorPlugins.

@khellste
Copy link
Author

khellste commented Oct 29, 2025

I considered ClassDB, but the docs state pretty plainly that script-defined classes are outside the purview of ClassDB:

Note: Script-defined classes with class_name are not part of ClassDB, so they will not return reflection data such as a method or property list. However, GDExtension-defined classes are part of ClassDB, so they will return reflection data.

Whether or not I agree with that design choice for ClassDB, I think it means it's not the right place to interface with metadata on script-defined classes. And in my opinion, this metadata feature will not be very useful if it can't be used on script-defined classes.

That said, I absolutely support something like bind_meta(...) so that metadata is settable outside of GDScript.

@HolonProduction
Copy link
Member

I don't consider this PR a solution to the linked proposal.

This syntax is utterly unreadable and I don't even understand why, given that the challenge with custom annotations certainly isn't the parsing part.

One of the big technical challenges for custom annotations is, that we don't have a way to resolve conflicts because we don't have namespacing. So if two plugins use the same annotation name we have a problem.

This PR doesn't solve this issue. It just pushes the responsibility onto plugin devs.

Hey plugin dev: here is an arbitrary dictionary that might or might not contain what you are looking for. Did the user make a mistake? Is there a conflicting plugin? Idk but I'm sure you'll figure it out!

We can't expect plugin devs to figure all this verification out themselves, otherwise implementing annoatations will be a huge mess.

It's also basically impossible to design good UX around free form metadata:

  • autocompletion? yeah no
  • Want errors ahead of runtime? nope, plugin dev has to verify at runtime, not our responsibility
  • want to read the docs? good luck figuring it out, there is no specific language construct involved, which could own the documentation
  • made a typo for the parameter? How should the engine know? The plug-in might just behave differently then you expected

All of those concerns are totally unrelated to the question of "key value metadata pairs" vs "python decorators". JVM annotations are also only key value pairs. But they are readable, have well defined parameters and can provide good UX.

@Frontrider
Copy link

Fails at readability. That is enough to make it not work.

Only way I can see it work if you allow some form of metaprogramming with it but that is also outside scope.

Metaprogramming here means something like junit5 annotations where you can put annotations on annotation classes to combine them. Which means custom annotations in the end.

@Shadows-of-Fire
Copy link
Contributor

I don't consider a single @meta tag a suitable solution to the issue of custom annotations. While it is able to drive the purpose that "annotations are metadata" it fails to create an extensible first-class annotation system, and hides the lack of extensibility in the single @meta annot.

Basically, having @meta forces what should look something like @annot1(params) into @meta(annot1, { params }) which is a mess, comparatively.

@khellste
Copy link
Author

Okay, I'm definitely sensing that this is not the thing people want and this needs to be fleshed out more.

That discussion is probably better to have back in the original proposal, godotengine/godot-proposals#1316.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants